package local.pt.iscte.dcti.pa.model;

import java.util.List;
import java.util.Observable;
import java.util.Observer;

import local.pt.iscte.dcti.pa.utils.Common;
import local.pt.iscte.dcti.pa.utils.ExtensionManager;

/**
 * Defines a sheet cell
 * @author Simão Fernandes
 * @version %I%, %G%
 * @since 1.0
 */
public class Cell extends Observable implements Observer {
	//ATTRIBUTES
	private int abscissa; // XX
	private int ordinate; // YY
	private String value = null;
	private WorksheetModel model;
	private ExtensionManager myExtentionManager;
	//CONSTRUCTORS
	
	/**
	 * Class constructor
	 * @param abscissa	Cell abscissa
	 * @param ordinate	Cell ordinate
	 * @param _model	Implementing object
	 * @param _myExtentionManager reference to ExtensionManager instance
	 */
	public Cell(int abscissa, int ordinate, WorksheetModel _model, ExtensionManager _myExtentionManager) {
		this.abscissa = abscissa;
		this.ordinate = ordinate;
		this.model = _model;
		this.myExtentionManager = _myExtentionManager;
	}
	
	//PUBLIC METHODS
	
	/**
	 * Gets the cell abscissa
	 * @return	Cell abscissa
	 */
	public int getAbscissa() {
		return abscissa;
	}
	
	/**
	 * Gets the cell ordinate
	 * @return Cell ordinate
	 */
	public int getOrdinate() {
		return ordinate;
	}
	
	/**
	 * Gets the cell value
	 * @return Cell value
	 */
	public String getValue() {

		return this.value;
	}
	
	/**
	 * Sets the cell value
	 * @param value
	 */
	public void setValue(String value) {
		this.value = value;
	}
	
	/**
	 * Sets an observer to instance
	 * @param obs Observer instance
	 */
	public void setObserver(Observer obs) {
		this.addObserver(obs);
	}
	
	/**
	 * Handles the changing of the observed instance
	 * @param o Reference to the observed instance
	 * @param arg Observed instance arguments 
	 */
	@Override
	public void update(Observable o, Object arg) {

		//Cell observableCell = (Cell) o;
		System.out.println("Handling cell: " + this.toString());
		String updatedValue = this.solveIt();
	}
	
	/**
	 * Solves the value of the current instance
	 * @return Result
	 */
	public String solveIt() {
		String newValue = this.value;
		List<String> operands = null;
		
		
		if (newValue.charAt(0) == '=') {

			newValue = newValue.substring(1); // retira o "="

			operands = Common.getMatches("([A-Z]+\\d+)", newValue);
			String cellValue = null;

				
			int[] coordinates;

			for (String operand : operands) {
				coordinates = Common.GetCoordinates(operand);

				try {

					cellValue = computeCellValue(coordinates);

				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

				newValue = newValue.replace(operand, cellValue);
			}
		}
		
		
		boolean isFunc = false;
		
		if (myExtentionManager.isExtensionMethod(newValue)) {
			isFunc = true;
			operands = Common.getMatches("\\((\\d*?)\\)", newValue);
			
			int auxOperand = Integer.parseInt(operands.get(0).replace("(", "").replace(")", ""));
			
			newValue = Integer.toString(myExtentionManager.applyFunction(newValue.substring(0, newValue.indexOf('(')), auxOperand));
		}
		
		if (!isFunc) {
			newValue = doMath(newValue);
		}
		
		setChanged();
		notifyObservers(newValue);

		return newValue;
	}

	/**
	 * Gets a string definition of the instance
	 * @return String definition
	 */
	@Override
	public String toString() {
		return String.format("Abscissa: %s; Ordinate: %s; Value: %s",
				this.getAbscissa(), this.getOrdinate(), this.getValue());
	}
	
	//PRIVATE METHODS
	
	/**
	 * Computes the value of a given cell
	 * @param coordinates Coordinates of the cell
	 * @return Result
	 * @throws Exception Thrown exception
	 */
	private String computeCellValue(int[] coordinates) throws Exception {
		Cell targetCell = model.getCell(coordinates[0], coordinates[1]);
		targetCell.addObserver(this);
		String cellValue = targetCell.getValue();

		// se for um numero
		if (Common.isDigit(cellValue)) {
			return cellValue;
		}
		
		
		List<String> operands;
		if (cellValue.charAt(0) == '=') {
			cellValue = cellValue.substring(1); // retira o "="
			operands = Common.getMatches("[A-Z]+\\d+", cellValue);
			String auxCellValue;
			for (String operand : operands) {
				auxCellValue = computeCellValue(Common.GetCoordinates(operand));
				cellValue = cellValue.replace(operand, auxCellValue);
			}
		}

		//-->
		boolean isFunc = false;
		
		if (myExtentionManager.isExtensionMethod(cellValue)) {
			isFunc = true;
			operands = Common.getMatches("\\((\\d*?)\\)", cellValue);
			
			int auxOperand = Integer.parseInt(operands.get(0).replace("(", "").replace(")", ""));
			
			cellValue = Integer.toString(myExtentionManager.applyFunction(cellValue.substring(0, cellValue.indexOf('(')), auxOperand));
		}
		
		if (!isFunc) {
			cellValue = doMath(cellValue);
		}
		//->
		
		return doMath(cellValue);
	}
	
	/**
	 * Does the math
	 * @param expression expression to do the math
	 * @return Result
	 */
	private String doMath(String expression) {

		int res = 0;

		List<String> matches;

		matches = Common.getMatches("\\d+([\\*/]\\d+)+", expression);

		for (String string : matches) {
			String result = getPrecedentResult(string);
			if (result == null) {
				return null;
			}
			expression = expression.replace(string, result);
		}

		//matches = Common.getMatches("\\-?\\d+([\\+\\-]\\d+)+", expression);
		matches = Common.getMatches("(\\-|\\+)?\\d+", expression);		//Ja nao ha produtos ou divisoes

		for (String part : matches) {
			res += Integer.parseInt(part);
		}

		return Integer.toString(res);
	}
	
	/**
	 * Does the precedent calculations
	 * @param expression expression to do the math
	 * @return Result
	 */ 
	private String getPrecedentResult(String expression) {

		List<String> matches = Common.getMatches("\\d|[\\*/]", expression);
		boolean doProduct = false, doDivision = false;
		int aux = 0;
		
		for (String macth : matches) {
			switch (macth) {
			case "*":
				doProduct = true;
				break;
			case "/":
				doDivision = true;
				break;
			default:
				if (doProduct) {
					aux *= Integer.parseInt(macth); 
					doProduct = false;
				}
				else if (doDivision) {
					if (Integer.parseInt(macth) == 0) {
						return null;
					}
					aux /= Integer.parseInt(macth);
					doDivision = false;
				}
				else {
					aux = Integer.parseInt(macth);
				}
				break;			
			}
		}
		return Integer.toString(aux);
	}
}
